﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Buddy.BehaviorTree;
using Buddy.Common;
using Buddy.Common.Math;
using Buddy.CommonBot;
using Buddy.Navigation;
using Buddy.Swtor;
using Buddy.Swtor.Objects;

using WingIt.Routines;
using WingIt.Routines.Mirrors;

using Action = Buddy.BehaviorTree.Action;


namespace WingIt
{
    [RevisionControlId("$Id: Movement.cs 695 2012-08-13 17:15:25Z Chinajade $")]
    public static partial class WingItCommon
    {
        /// <summary>
        /// <para>Move within MAXRANGE and within line-of-sight of the target identified by DESTINATIONUNIT.</para>
        /// <para>Notes:<list type="bullet">
        /// <item><description><para> * MAXRANGE must be greater than or equal to 0.0f.</para></description></item>
        /// <item><description><para> * When this behavior completes, the character may occasionally be in motion for a short time after.
        /// If you want to completely stop, follow this behavior with a StopMoving() behavior.</para></description></item>
        /// </list></para>
        /// </summary>
        /// <param name="targetDelegate"></param>
        /// <param name="maxRange"></param>
        /// <param name="doWhen"></param>
        /// <returns>true, if action is in progress.  Returns false, if action is not needed.</returns>
        ///  8Jul2012-17:13UTC chinajade
        // NB: It is tempting to include a StopMoving() as part of ths behavior; however, please refrain from doing so.
        // Not all classes require a cessation of movement before they can start their attacks (e.g., insta-casts).
        // Instead, the caller should explicitly follow calls of this behavior with a CreateBTPS.StopMoving() behavior,
        // if that is what the caller requires.
        public static bool MoveWithinRangeAndLoS(TorCharacter destinationUnit, float maxRange)
        {
            if (!LazyRaider.IsMovementEnabled) return false;

            if (destinationUnit == null) return false;
            if ((destinationUnit.Distance <= maxRange) && destinationUnit.InLineOfSight) return false;

            // We're not in range or LoS...
            if (_moveWithinRangeAndLoS_Throttle.IsReady())
            {
                Navigator.MoveTo(destinationUnit.Position);
                _moveWithinRangeAndLoS_Throttle.Used(_moveWithinRangeAndLos_RetryDuration);
            }

            return true;
        }

        private static TimeSpan _moveWithinRangeAndLos_RetryDuration = TimeSpan.FromMilliseconds(100.0);
        private static Throttle _moveWithinRangeAndLoS_Throttle = new Throttle_Timed("MoveWithinRangeAndLoS");


        /// <summary>
        /// <para>Stops toon movement when it is within MAXRANGE and line-of-sight.</para>
        /// <para>Notes:<list type="bullet">
        /// <item><description><para> * Attempts to stop toon movement may fail in some situations.
        /// For instance, the toon may be sliding down a hill.
        /// When throttling happens, a message is emitted to the log.</para></description></item>
        /// <item><description><para> * Failure to stop moving can happen due to lag, client stuttering, low frame rates, game terrain, and other things.</para></description></item>
        /// </list></para>
        /// </summary>
        /// <returns>true, if action is in progress.  Returns false, if action is not needed.</returns>
        ///  7Jul2012-20:27UTC chinajade
        public static bool StopWithinRangeAndLoS(TorCharacter destinationUnit, float maxRange)
        {
            if (!LazyRaider.IsMovementEnabled) return false;

            if (destinationUnit == null) return false;
            if ((destinationUnit.Distance <= maxRange) && destinationUnit.InLineOfSight && !Me.IsMoving) return false;

            if (_stopWithinRangeAndLoS_Throttle.IsReady())
            {
                // Double-Condom approach - tried many, many, ways of stopping,
                // this is the ONLY RELIABLE way of doing it found to-date
                Navigator.MovementProvider.StopMovement();
                Buddy.Swtor.Movement.Stop(MovementDirection.Forward);
                Buddy.Swtor.Input.MoveStopAll();
            }

            return true;
        }

        private static TimeSpan _stopWithinRangeAndLos_RetryDuration = TimeSpan.FromMilliseconds(100.0);
        private static Throttle _stopWithinRangeAndLoS_Throttle = new Throttle_Timed("StopWithinRangeAndLoS");

        /// <summary>
        /// <para>Crouches in a location appropriate to attack the TARGETUNIT.</para>
        /// <para>The behavior takes whatever actions necessary to move within range and line-of-sight of the target, before crouching.</para>
        /// <para>Notes:<list type="bullet">
        /// <item><description><para> * If DOWHEN is omitted, the behavior will always be attempted.</para></description></item>
        /// <item><description><para> * This behavior is only for Agents and Smuggler use.
        /// Use by any other class will result in a MaintenanceError.</para></description></item>
        /// <item><description><para> * Crouching will sometimes fail.
        /// This behavior throttles failed attempts to crouch so other activities can take place.
        /// When throttling happens, a message is emitted to the log.</para></description></item>
        /// <item><description><para> * Failure to crouch can happen due to lag, client stuttering, low frame rates, game terrain, and other things.</para></description></item>
        /// </list></para>
        /// </summary>
        /// <param name="targetUnit"></param>
        /// <returns>true, if action is in progress.  Returns false, if action is not needed.</returns>
        ///  8Aug2012-06:25UTC chinajade
        public static bool MoveAndCoverForTarget(TorCharacter targetUnit, float maxRange)
        {
            const int MaxCover_TryCount = 3;
            const int MaxCover_VisitsCountAtPull = 2;

            if (!((Me.Class == CharacterClass.Agent) || (Me.Class == CharacterClass.Smuggler)))
            {
                Logger.MaintenanceError("The {0} behavior is only useful for Agent- or Smuggler-based classes."
                                        + "\n    Your class is {1}.", Logger.GetMyMethodName(), Me.Class);
            }

            if (!LazyRaider.IsMovementEnabled) return false;

            if (targetUnit == null) return false;

            // This is built as a rather complex state-machine, because all the bugs in SWtOR made us do it this way...
            // As a general rule, the state machine should report RunStatus.Success to the BT parent whenever it changes state; otherwise,
            // it reports RunStatus.Failure.
            // NB: When altering the state machine, please recall that our state may go 'stale' due to external influences--
            // such as being knocked out of cover, or one of our explosive attacks knocking our target out of range.
            // The state machine must detect these situations and recover gracefully.

            REEVALUATE_STATE:
            //Info we need when people report problems...
            //Logger.Debug("LoSResult:{0} CoverState:{1}({2}), '{3}' at {4:f1}{5} meters",
            //    Me.IsAbilityReady(BuddyTor.Me.GetMirrorAbility("Rifle Shot"), targetUnit).ToString(),
            //    _coverState.ToString(),
            //    (Me.HasMirrorBuff("Cover") ? "TakeCover" : Me.HasMirrorBuff("Crouch") ? "Crouch" : "None"),
            //    targetUnit.LoggableIdentifier(),
            //    (targetUnit.Distance * 10),
            //    ((_coverState == CoverState.ClosingDistance) ? string.Format("/{0:f1}({1})", _coverDistanceNeeded * 10, (targetUnit.InLineOfSight ? "LoS" : "No-LoS")) : "")
            //    );
            switch (_coverState)
            {
                case CoverState.Covered: {
                    _tryCount_CoverAttempts = 0;

                    // If cover not sufficient to address current target...
                    // NB: simply changing targets is insufficient to declare cover as no longer valid.
                    if (!Me.IsInCover() || !targetUnit.IsInRange(maxRange) || !targetUnit.InLineOfSight)
                    {
                        _coverState = CoverState.UnCovered;
                        goto REEVALUATE_STATE;
                    }

                    // This detects terrain glitches, and moves closer to mob if needed...
                    if (IsBugged_TargetViewBlocked(targetUnit))
                    {
                        Logger.Write("Target view blocked for '{0}'--recovering by closing distance", targetUnit.LoggableIdentifier());
                        _coverState = CoverState.ChokeUpNeeded;
                        goto REEVALUATE_STATE;
                    }

                    // This detects when we're unable to acquire the target while covered at the pull...
                    // This method detects a particular SWtOR glitch that looks like this: the toon will TakeCover/Crouch,
                    // but every time it tries to use an ability, a "Cannot See Target" message will show up on the screen.
                    // From a programming viewpoint, this situation is not directly detectable (the EffectResult return code
                    // from the ability use is 'Unknown').  This situation only happens 'at the pull'.
                    // The technique we use here is a brute-force one, that only looks when we're at the pull.  If we visit a few times
                    // without getting into combat, this method decides the bug is active, and notifies the caller for corrective action.
                    if (TargetSelect.IsAtThePull())
                    {
                        if (++_coverStateVisitsAtThePull > MaxCover_VisitsCountAtPull)
                        {
                            _coverStateVisitsAtThePull = 0;
                            _coverState = CoverState.ChokeUpNeeded;
                            goto REEVALUATE_STATE;
                        }
                    }

                    _coverStateVisitsAtThePull = 0;
                    return false;
                }

                case CoverState.UnCovered: {
                    StopWithinRangeAndLoS(targetUnit, float.MaxValue);
                    // REVISIT: Mirror.Cast(_abilityTakeCover.Name), // forces a dismount, if needed

                    _lastCoverTarget = targetUnit;

                    // If we've exceeded our tries to take cover, close some distance before trying again...
                    // There are certain places on the terrain where TakeCover/Crouch appears to succeed, but it indeed fails (we
                    // never acquire the buff).  When this happens, we need to move.
                    if (_tryCount_CoverAttempts > MaxCover_TryCount)
                    {
                        _tryCount_CoverAttempts = 0;
                        _coverState = CoverState.ChokeUpNeeded;
                        goto REEVALUATE_STATE;
                    }

                    // If not in range or LoS, close the distance...
                    if (!_lastCoverTarget.IsInRange(maxRange) || !_lastCoverTarget.InLineOfSight)
                    {
                        _coverDistanceNeeded = maxRange;
                        _coverState = CoverState.ClosingDistance;
                        goto REEVALUATE_STATE;
                    }

                    // The IsAbilityReady() is used to detect and work around LoS problems caused by terrain glitches...
                    if (IsBugged_TargetViewBlocked(_lastCoverTarget))
                    {
                        _coverState = CoverState.ChokeUpNeeded;
                        goto REEVALUATE_STATE;
                    }

                    // If existing cover handles new target, we're good...
                    if (Me.IsInCover())
                    {
                        _coverState = CoverState.Covered;
                        goto REEVALUATE_STATE;
                    }

                    _coverState = CoverState.TryTakeCoverAndCrouch;
                    goto REEVALUATE_STATE;
                }

                case CoverState.TryTakeCoverAndCrouch: {
                    ++_tryCount_CoverAttempts;
                    // Make certain we're targeting the target such that Take Cover position will be calculated correctly...
                    // The TargetSelect.ForAttack() target may not be the same as the CurrentTarget is the reason we must do this.
                    _lastCoverTarget.Target();

                    // Prefer "Take Cover"...
                    EffectResult coverResult = EffectResult.Unknown;
                    Logging.Write("Casting " + _abilityTakeCover.Name);
                    BuddyTor.Me.AbilityActivate(_abilityTakeCover, Me, out coverResult);
                    if (coverResult != EffectResult.Ok)
                    {
                        // "Take Cover" failed, try "Crouch"...
                        coverResult = EffectResult.Unknown;
                        Logging.Write("Casting " + _abilityCrouch.Name);
                        BuddyTor.Me.AbilityActivate(_abilityCrouch, Me, out coverResult);
                    }

                    if ((coverResult == EffectResult.Ok) && _lastCoverTarget.InLineOfSight)
                    {
                        _coverState = CoverState.Covered;
                        goto REEVALUATE_STATE;
                    }

                    _coverState = CoverState.ChokeUpNeeded;
                    goto REEVALUATE_STATE;
                }

                case CoverState.ChokeUpNeeded:  {
                    // TakeCover/Crouch failed or target not in LoS; Close the distance to the target...
                    // TakeCover/Crouch occasionally fails if we're standing on steps or right up against a wall.
                    // The Target LoS can be broken on TakeCover/Crouch for several reasons.  For example,
                    // 1) The target is slightly elevated, and we're too close to the dias base on which the target is standing, or
                    // 2) There is a barrel/box between us and target that is LoS-okay while we're standing, but LoS-impaired
                    //    once we TakeCover/Crouch.
                    // 3) Sometimes there are simply 'landscape glitches' that cause the attack to produce EffectResult.TargetViewBlocked.
                    _coverDistanceNeeded = CalculateChokeDistance(targetUnit.Distance);
                    _coverState = CoverState.ClosingDistance;
                    goto REEVALUATE_STATE;
                }

                case CoverState.ClosingDistance: {
                    // If target changed, we may not need to close distance, or may need a new choke distance...
                    if (targetUnit != _lastCoverTarget)
                    {
                        _coverState = CoverState.UnCovered;
                        goto REEVALUATE_STATE;
                    }

                    if (targetUnit.IsInRange(_coverDistanceNeeded) && targetUnit.InLineOfSight)
                    {
                        _coverState = CoverState.UnCovered;
                        goto REEVALUATE_STATE;
                    }

                    if (targetUnit.IsInRange(Global.Distance.Melee * 0.75f))
                    {
                        _coverState = CoverState.CoverFailed;
                        goto REEVALUATE_STATE;
                    }

                    // REVISIT: CreateBTPS.BreakCrouchOrCover(),
                    MoveWithinRangeAndLoS(targetUnit, _coverDistanceNeeded);
                    return true;
                }

                case CoverState.CoverFailed: {
                    if (!Me.InCombat || (targetUnit != _lastCoverTarget))
                    {
                        _coverState = CoverState.UnCovered;
                        goto REEVALUATE_STATE;
                    }

                    if (Me.IsInCover())
                    {
                        _coverState = CoverState.Covered;
                        goto REEVALUATE_STATE;
                    }

                    return false;
                }

                default: {
                    Logger.MaintenanceError("Unhandled state '{0}' in {1}", _coverState.ToString(), Logger.GetMyMethodName());
                    break;
                }
            }

            // REVISIT: Mirror.Cast(_abilityCrouch.Name, crouchOn => Me);
            return false;
        }

        private static TorAbility _abilityCrouch = BuddyTor.Me.GetMirrorAbility("Crouch");
        private static TorAbility _abilityTakeCover = BuddyTor.Me.GetMirrorAbility("Take Cover");
        private static float _coverDistanceNeeded = Global.Distance.Ranged;
        private static CoverState _coverState = CoverState.UnCovered;
        private static int _coverStateVisitsAtThePull = 0;
        private static TorCharacter _lastCoverTarget = null;
        private static int _tryCount_CoverAttempts = 0;

        private enum CoverState
        {
            ChokeUpNeeded,
            ClosingDistance,
            Covered,
            CoverFailed,
            TryTakeCoverAndCrouch,
            UnCovered,
        };

        private static float CalculateChokeDistance(float currentDistance)
        { // 8Aug2012-11:51UTC chinajade
            const float DistanceChokeupMin = 0.3f; // minimum absolute move distance to get closer to target (in decameters)
            const float DistanceChokeupPercent = 0.20f; // percentage move distance to get closer to target
            const float MinDistanceAllowedToTarget = Global.Distance.Melee * 0.75f;

            float distanceByAbsoluteCalculation = currentDistance - DistanceChokeupMin;
            float distanceByPercentageCalculation = currentDistance * (1.0f - DistanceChokeupPercent);

            // NB: We should never return a very tiny or negative distance, thus the MinDistanceAllowedToTarget hard stop...
            return Math.Max(MinDistanceAllowedToTarget, Math.Min(distanceByAbsoluteCalculation, distanceByPercentageCalculation));
        }

        // This method detects a particular SWtOR glitch that looks like this: the SWtOR client reports us
        // as having line-of-sight on the target; however, trying to use an ability returns a TargetViewBlocked
        // result.
        // A target can be InLineOfSight, yet an attack can return a TargetViewBlocked error--go figure.
        // This detects the presence of the dichotomy.
        // 13Aug2012-06:50UTC chinajade
        private static bool IsBugged_TargetViewBlocked(TorCharacter target)
        {
            TorAbility testLosAbility = BuddyTor.Me.GetMirrorAbility("Rifle Shot");

            return Me.IsAbilityReady(testLosAbility, target) == EffectResult.TargetViewBlocked;
        }
    }
}
